/*
 *  liquidity/actor-model.vala
 *
 *  Copyright (c) 2008 Patrick Walton <pcwalton@uchicago.edu>
 */

using GLib;
using Gee;
using Clutter;

namespace Liquidity {
    public class ActorModel : Object, IAnimatable {
        private HashMap<string, Animation> _implicit_animations;

        protected ContainerModel _supermodel;
        public ContainerModel supermodel {
            get {
                return _supermodel;
            }
            set {
                if (_supermodel != null)
                    _supermodel._remove_submodel(this);
                assert(_supermodel == null);

                value.add_submodel(this);
                assert(_supermodel == value);
            }
        }

        public void _set_supermodel(ContainerModel sm)
        {
            _supermodel = sm;
        }

        public void remove_from_supermodel()
        {
            stdout.printf("remove_from_supermodel\n");
            _supermodel._remove_submodel(this);
        }

        protected virtual void _set_actor(Actor val)
        {
            _actor = val;

            /*
             *  Set up the model properties to match the current properties of
             *  the presentation actor.
             */ 
            _position = new Point((uint)_actor.get_xu(), (uint)
                    _actor.get_yu());
            _size = new Size((uint)_actor.get_widthu(),
                    (uint)_actor.get_heightu());
            Unit ux, uy;
            _actor.get_anchor_pointu(out ux, out uy);
            _anchor_point = new Point((uint)ux, (uint)uy);
        }

        protected Actor _actor;
        public Actor actor {
            get {
                return _actor;
            }
            protected set {
                _set_actor(value);
            }
        }

        public ActorModel(Actor a)
        {
            actor = a;
        }

        construct {
            _implicit_animations = new HashMap<string, Animation>();
        }

        private Point _position;
        public Point position {
            get {
                return _position;
            }
            set {
                _position = value;
                Transaction._add_update(new Update(this, "position"));
            }
        }

        /*
         *  Clutter anchor points aren't scaled; i.e., they specify the actual
         *  offset in pixels from the top left corner of the actor to the
         *  anchor point. By contrast, our anchor points are scaled from 0.0 to
         *  1.0. This function updates the actor's anchor point to conform to
         *  our own. It needs to be called whenever the size or the anchor
         *  point changes.
         */
        private void update_anchor_point()
        {
            actor.set_anchor_pointu(
                    (Unit)UnitFunctions.fixed_multiply(_size.width,
                        _anchor_point.x),
                    (Unit)UnitFunctions.fixed_multiply(_size.height,
                        _anchor_point.y));
        }

        private Size _size;
        public Size size {
            get {
                return _size;
            }
            set {
                _size = value;
                /* TODO: animate */
                // Transaction._add_update(new Update(this, "size"));
                actor.set_sizeu((Unit)_size.width, (Unit)_size.height);
                update_anchor_point();
            }
        }

        private Point _anchor_point;
        public Point anchor_point {
            get {
                return _anchor_point;
            }
            set {
                _anchor_point = value;
                /* TODO: animate */
                update_anchor_point();
            }
        }

        public Rect# frame {
            get {
                return new Rect(
                        _position.x -
                        UnitFunctions.fixed_multiply(_anchor_point.x,
                            _size.width),
                        _position.y -
                        UnitFunctions.fixed_multiply(_anchor_point.y,
                            _size.height),
                        _size.width,
                        _size.height);
            }
            set {
                Rect f = value;

                size = f.size;
                position = new Point(
                        f.origin.x + UnitFunctions.fixed_multiply(
                            _anchor_point.x, f.size.width),
                        f.origin.y + UnitFunctions.fixed_multiply(
                            _anchor_point.y, f.size.height));
            }
        }

        public void _add_implicit_animation(string kp, Animation anim)
        {
            assert(!_implicit_animations.contains(kp));

            _implicit_animations.set(kp, anim);
            anim.timeline.start();
        }

        public void _delete_implicit_animation(string kp)
        {
            if (_implicit_animations.contains(kp)) {
                Animation anim = _implicit_animations.get(kp);
                anim.timeline.stop();
                _implicit_animations.remove(kp);
            }
        }

        public void _add_actor_to_parent(Timeline tl, int frame_no)
        {
            if (frame_no == 1)
                ((Container)_supermodel.actor).add_actor(_actor);
        }

        public void _remove_actor_from_parent(Timeline tl)
        {
            ((Container)_actor.get_parent()).remove_actor(_actor);
        }

        /*
         *  Returns a properly initialized behavior for the given key path.
         *  This is where the majority of the implicit animation logic resides.
         */
        public virtual Behaviour get_behavior_for_key_path(string kp, Alpha
                alpha, Timeline timeline)
        {
            switch (kp) {
                case "added":
                    timeline.new_frame += _add_actor_to_parent;
                    return new BehaviourOpacity(alpha, 0, 255);

                case "removed":
                    /* FIXME: merge this with add */
                    timeline.completed += _remove_actor_from_parent;
                    return new BehaviourOpacity(alpha, 255, 0); 

                case "position":
                    Knot[] knots = new Knot[2];
                    knots[0].x = actor.get_x();
                    knots[0].y = actor.get_y();

                    /* FIXME: terrible terrible hack! Really should define a
                     * new Behavior that takes Units instead of pixels... */
                    knots[1].x = (int)(_position.x) >> 16;
                    knots[1].y = (int)(_position.y) >> 16;

                    return new BehaviourPath(alpha, knots, knots.length);

                case "size":
                    return null;    /* TODO */
            }

            /* Ask the filter if it supports the key path. */
            if (_filter != null) {
                Behaviour b = _filter.get_behavior_for_key_path(kp, alpha,
                        timeline);
                if (b != null)
                    return b;
            }

            return null;
        }

        private Filter _filter = null;
        public Filter filter {
            get {
                return _filter;
            }
            set {
                if (_filter != null)
                    _filter._remove_from_actor_model();

                _filter = value;
                _filter._set_actor_model(this);

                /* TODO: start an animation */
            }
        }

        private ArrayList<Constraint> _constraints;
        public ArrayList<Constraint> constraints {
            get {
                return _constraints;
            }
            construct {
                _constraints = new ArrayList<Constraint>();
            }
        }
    }
}

